#!/usr/bin/env shoes

# Copyright (c) 2008-2009  Dmitry A. Ustalov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.

DEBUG = true if ENV['DEBUG']

CALENDAR = [
  '',
  'Январь',
  'Февраль',
  'Март',
  'Апрель',
  'Май',
  'Июнь',
  'Июль',
  'Август',
  'Сентябрь',
  'Октябрь',
  'Ноябрь',
  'Декабрь'
]

Shoes.setup do
  gem 'sqlite3-ruby'
  gem 'sequel >= 2.10.0'
  gem 'hoe >= 1.8.3' # for multibyte
  gem 'multibyte >= 0.1.1'
end

require 'multibyte'
require 'sqlite3'
require 'sqlite3/version'
require 'sequel'
require 'logger'

class Table < Shoes::Widget # The Tablie
  def initialize args, &block
    @row_sizes = args[:rows]
    @control_stack = stack
    instance_eval &block if block_given?
  end
  def row
    @control_stack.append do
      r = @control_stack.contents.size
      flow do
        @row_sizes.each_with_index { |w, c|
          stack :width => w do
            yield r, c if block_given?
          end
        }
      end
    end
  end
end

class Monethus < Shoes
  url '/', :stat
  url '/about', :about
  url '/(\d+)', :month

  module Images
    DELETE = 'icons/edit-clear.png'
    EDIT = 'icons/edit-select-all.png'
  end

  DATA_DIRECTORY = begin
    if RUBY_PLATFORM =~ /win32/
      if ENV['APPDATA']
        File.join File.expand_path( ENV['APPDATA'] ), 'Monethus'
      elsif ENV['USERPROFILE']
        File.join File.expand_path( ENV['USERPROFILE'] ), 'Monethus'
      else
        File.expand_path Dir.getwd
      end
    else
      File.expand_path File.join( '~', '.monethus' )
    end
  end

  unless File.exist? DATA_DIRECTORY
    Dir.mkdir DATA_DIRECTORY
  end

  DB_PATH = File.join( DATA_DIRECTORY, 'monethus.db' )

  DB = Sequel.sqlite DB_PATH
  # I know 'bout logger() method should only be used for
  # backwards compatibility.
  # But we have other choices? Currently, no.
  DB.logger = Logger.new( STDOUT ) if defined? DEBUG

  def months *styles
    stack( *styles ) do
      para strong( link( 'Статистика', :click => '/' ) )

      last_year = nil
      layout = []
      release_layout = proc {
        unless layout.empty?
          layout.pop
          layout.push '.'
          inscription span( *layout )
          layout = []
        end
      }

      Month.reverse_order( :year, :month ).all do |m|
        unless last_year.eql? m.year
          release_layout[]
          layout.push strong( m.year, ': ' )
          last_year = m.year
        end
        layout.push link( m.human_name, :click => "/#{m.id}" ), ', '
      end
      release_layout[]

      para link( 'О программе', :click => '/about' )
    end
  end

  def about
    months :width => 0.25

    stack :width => 0.75, :margin_right => 15 do
      tagline 'О программе'

      para strong( "Monethus #{Version::STRING}" ), ' ',
        'помогает вести свой личный бюджет.'
      para strong( 'Сайт проекта: ' ),
        link( 'http://code.google.com/p/monethus/' )
      para strong( 'Автор: ' ), 'Dmitry A. Ustalov (aka ',
        em( 'eveel' ), '), ведёт свой блог по адресу: ',
        link( 'http://eveel.blogspot.com/' ), '.'

      tagline 'Средства'
      para strong( 'Ruby' ), ' ', RUBY_VERSION
      para strong( 'Shoes' ), ' ', Shoes::RELEASE_NAME, ' ',
        "(r#{Shoes::REVISION})"
      para strong( 'SQLite3/Ruby' ), ' ',
        SQLite3::Version::STRING
      para 'Используются значки из проекта ' ,
        strong( 'Tango Desktop Project' ), ': ',
        link( 'http://tango.freedesktop.org/' ), '.'

      tagline 'База данных'
      para strong( 'Файл: ' ), DB_PATH

      if defined? DEBUG
        tagline 'Отладка'
        para 'Режим отладки включен.'
      end
    end
  end

  def stat
    months :width => 0.25

    stack :width => 0.75, :margin_right => 15 do
      tagline 'Общая статистика'

      layout = table :rows => [ 0.1, 0.45, 0.35, 0.1 ]
      last_year = nil
      balance = 0.00

      Month.reverse_order( :year, :month ).all { |m|
        monthly = m.balance

        layout.row do |r, c|
          case c
            when 0
              inscription m.year,
                :align => 'right' unless last_year.eql? m.year
              last_year = m.year
            when 1
              para link( m.human_name, :click => "/#{m.id}" )
            when 2
              para financial( monthly )
            when 3
              image Images::DELETE do
                if confirm 'Удалить месяц?'
                  begin
                    m.destroy
                    visit '/'
                  rescue => ex
                    DB.logger.error( ex )
                    alert 'Ошибка при удалении.'
                  end
                end
              end
          end
        end
        balance += monthly
      }

      button 'Добавить...' do
        year = ask( 'Год:' )
        month = ask( 'Месяц:' ) unless year.nil?

        unless year.nil? || month.nil?
          month.downcase!

          CALENDAR.each_with_index { |e, i|
            month = i if month.eql? e.downcase
          }

          begin
            m = Month.create :year => year, :month => month
            visit "/#{m.id}"
          rescue Sequel::Error::InvalidValue => ex
            DB.logger.error ex.message
            alert ex.message
          rescue => ex
            DB.logger.error ex
            alert 'Ошибка при сохранении.'
          end
        end
      end

      para strong( 'Баланс: ' ), financial( balance )
    end
  end

  def month id
    id = id.to_i

    months :width => 0.25

    month = Month.find :id => id
    monthly = month.balance
    balance = monthly + month.all_before.inject( 0.00 ) { |r, m|
      r + m.balance
    }

    stack :width => 0.75, :margin_right => 15 do
      flow do
        tagline "#{month.human_name} #{month.year}"

        inscription link( 'текст', :click => proc {
            name = ask_save_file

            unless name.nil?
              File.open( name, 'w' ) do |f|
                f.puts "# #{month.human_name} #{month.year} #"

                last_day = nil
                month.finances.each { |e|
                  f.puts "*#{daily( e.day )}*" unless last_day.eql? e.day
                  f.puts "\t#{financial( e.amount )}\t#{e.title}"
                  last_day = e.day
                }

                f.puts
                f.puts "## Итого ##"
                f.puts "За месяц: #{financial( monthly )}"
                f.puts "Баланс  : #{financial( balance )}"
                f.close
              end
            end
          })
      end

      layout = table :rows => [ 0.05, 0.55, 0.3, 0.1 ]
      last_day = nil

      month.finances.each do |f|
        layout.row do |r, c|
          case c
            when 0
              inscription daily( f.day ),
                :align => 'right' unless last_day.eql? f.day
              last_day = f.day
            when 1
              para strong( f.title )
            when 2
              para financial( f.amount )
            when 3
              flow do
                image Images::DELETE do
                  if confirm 'Удалить запись?'
                    begin
                      f.destroy
                      visit "/#{month.id}"
                    rescue => ex
                      DB.logger.error ex
                      alert 'Ошибка при удалении.'
                    end
                  end
                end
                image Images::EDIT do
                  day = ask( 'День:' )
                  amount = ask( 'Сумма:' ) unless day.nil?

                  unless day.nil? || amount.nil?
                    begin
                      f.day = day
                      f.amount = amount
                      f.save_changes
                    rescue Sequel::Error::InvalidValue => ex
                      DB.logger.error ex
                      alert ex.message
                    rescue => ex
                      DB.logger.error ex
                      alert 'Ошибка при сохранении.'
                    end
                    visit "/#{month.id}"
                  end
                end
              end
          end
        end
      end

      button 'Добавить...' do
        day = ask( 'День:' )
        title = ask( 'Название:' ) unless day.nil?
        amount = ask( 'Сумма:' ) unless title.nil?

        unless day.nil? || title.nil? || amount.nil?
          begin
            Finance.new do |f|
              f.month = month
              f.title = title
              f.amount = amount
              f.day = day
              f.save
            end
            visit "/#{month.id}"
          rescue Sequel::Error::InvalidValue => ex
            DB.logger.error ex
            alert ex.message
          rescue => ex
            DB.logger.error ex
            alert 'Ошибка при сохранении.'
          end
        end
      end

      para strong( 'За месяц: ' ), financial( monthly )
      para strong( 'Баланс: ' ), financial( balance )
    end
  end

  private
  class Version
    MAJOR = 0
    MINOR = 0
    THIRD = 7
    STRING = [ MAJOR, MINOR, THIRD ].join '.'
  end

  def daily day
    '%.2d' % day
  end

  def financial summ
    sign = begin
      if summ == 0.00
        ''
      else
        summ > 0.00 ? '+' : '–'
      end
    end
    "#{sign}%0.2f руб." % summ.abs
  end
end

class ::Month < Sequel::Model
  set_schema do
    primary_key :id
    integer :year
    integer :month
  end

  create_table unless table_exists?

  before_destroy do
    finances.each { |f| f.destroy }
  end

  validates do
    numericality_of :month, :only_integer => true
    numericality_of :year, :only_integer => true
  end

  has_many :finances, :order => :day

  def human_name
    CALENDAR[month]
  end

  def balance
    finances.inject( 0.00 ) { |r, i| r + i.amount }
  end

  def all_before
    Month.filter( { :year => year, :month => ( 0..month - 1 ) } |
      ( :year < year ) )
  end
end

class ::Finance < Sequel::Model
  set_schema do
    primary_key :id
    varchar :title, :size => 128
    real :amount
    integer :day
    foreign_key :month_id, :months
  end

  create_table unless table_exists?

  validates do
    presence_of :title
    numericality_of :amount
    numericality_of :day, :only_integer => true
    presence_of :month
  end

  belongs_to :month
end

Monethus.app :width => 530, :height => 500, :title => 'Monethus'

# vim: filetype=ruby:softtabstop=2:shiftwidth=2:expandtab
